package tv.geir.slackspace.common;

import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Set;
import java.util.Map.Entry;

import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import tv.geir.slackspace.code.BytecodeRetrievalException;
import tv.geir.slackspace.code.CachingBytecodeFinder;
import tv.geir.slackspace.code.ClassDefinition;
import tv.geir.slackspace.code.ClassLoadingObjectInputStream;
import tv.geir.slackspace.code.ProminentClassLoader;
import tv.geir.slackspace.code.ProminentObjectException;
import tv.geir.slackspace.matching.Template;
import tv.geir.slackspace.server.ServerLease;

/**
 * A mobile object contains a serialized object and class definitions necessary
 * to instantiate this object.
 * 
 * @author Geir Gullestad Pettersen
 * 
 */
public final class ProminentObject implements Serializable {
	/**
	 * Logger for this class
	 */
	private static Log log = LogFactory.getLog(ProminentObject.class);

	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;
	
	private transient ProminentClassLoader classLoader;

	private byte[] serializedObject;

	private LinkedHashMap<String, ClassDefinition> classDefinitions;

	private boolean enforceReloadClasses = false;

	private Template template;
	
	private ServerLease lease;

	private transient CachingBytecodeFinder bytecodeFinder = CachingBytecodeFinder.getInstance();
 
	private transient Serializable instance = null;

	private static transient ObjectSerializer serializer = new ObjectSerializer();
	
	public ServerLease getLease() {
		return lease;
	}

	public void setLease(ServerLease lease) {
		this.lease = lease;
	}

	
	
	public ProminentObject(Serializable object, boolean enforceClassReloading) {

		if (object == null) {
			throw new IllegalArgumentException(
					"object to serialize can't be null");
		}

		classDefinitions = new LinkedHashMap<String, ClassDefinition>();
	
		this.enforceReloadClasses = enforceClassReloading;
		
		template = new Template( object, true );
		instance = object;
		serializedObject = serializer.serialize( instance );
	}

	public ProminentObject(Serializable object) {
		this(object, false);
	}


	public  byte[] getSerializedObject() {
		// serialize instance if not serialized.
		if( serializedObject == null ) {
			throw new NullPointerException( "Byte array representing serialized object is null" );
		}
		synchronized (serializedObject) {
			if( instance != null &&
					(serializedObject == null || 
					 serializedObject.length > 0)) {
				serializedObject = serializer.serialize( instance );
			}
		}
		return serializedObject;
	}

	/**
	 * Adds all classdefinitions in <code>defs</code> to this object's
	 * classdefinition list. Any matching exising definitions will
	 * be overwritten.
	 * @param defs
	 */
	public void addClassDefinitions( Collection<ClassDefinition> defs ) {
		if( defs == null ) {
			return;
		}
		
		for( Iterator<ClassDefinition> i=defs.iterator(); i.hasNext(); ) {
			ClassDefinition cd = i.next();
			if( cd != null ) {
				classDefinitions.put( cd.getClassName(), cd );
			}
		}
	}
	
	/**
	 * Adds <code>c</code>'s class definition (bytecode and class name) to
	 * this mobile object's list of class dependencies.
	 * 
	 * @param c
	 * @throws <code>BytecodeNotFoundException</code>
	 */
	public void addClassDefinition(Class c) throws BytecodeRetrievalException{
		if (c == null) {
			throw new NullPointerException("class can't be null");
		}

		String classname = c.getName();
		if (!classDefinitions.containsKey(classname)) {
		
			ClassDefinition cd = bytecodeFinder.findClassDefinition(classname);
			classDefinitions.put(classname, cd);

			if( log.isDebugEnabled() ) {
				log.debug(classname + " definition added to mobile object");
			}
		}
	}

	public void addClassDefinition(ClassDefinition cd) {
		classDefinitions.put(cd.getClassName(), cd);
	}

	public Set<Entry<String, ClassDefinition>> getClassDefinitions() {
		return classDefinitions.entrySet();
	}



	/**
	 * 
	 * @param classRequestor
	 * @return
	 * @throws Exception
	 */
	public synchronized Serializable deSerialize()  {
		if( instance != null ) {
			return instance;
		}
		
		// add class definitions to class loader
		classLoader.addClassDefinitions(classDefinitions.entrySet(), enforceReloadClasses);

		
		// deserialize object
		ByteArrayInputStream bais = new ByteArrayInputStream(serializedObject);
		
		try {
			ObjectInputStream ois = new ClassLoadingObjectInputStream(classLoader, bais);
			Serializable s;

			s = (Serializable) ois.readObject();
			
			ois.close();
			bais.close();
			
			instance = s;
			if(log.isDebugEnabled()) {
				log.debug("Object deserialized");
			}
			return s;
		} catch( Throwable t ) {
			throw new ProminentObjectException( "Failed deserialization", t );
		}
	}
	public ProminentClassLoader getClassLoader() {
		return classLoader;
	}

	public void setClassLoader(ProminentClassLoader classLoader) {
		this.classLoader = classLoader;
	}


	public Template getTemplate() {

		if (template == null) {
			// template = new Template( deSerialize() );
		}
		return template;
	}

	public void setTemplate(Template template) {
		this.template = template;
	}
	
	@Override
	public String toString() {
		int bytes = serializedObject != null ? serializedObject.length : 0 ;
		ToStringBuilder s = new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE);
		s.append("objectSize", bytes );
		s.append("template", template );
		s.append("classDefinitionsMap", classDefinitions);
		s.append("byteCodeSize", definitionsBytesSize() );
		
		return s.toString();
	}
	
	public String toStringShort() {
		int bytes = serializedObject != null ? serializedObject.length : 0 ;
		ToStringBuilder s = new ToStringBuilder(null);
		s.append("objectSize", bytes );		
		s.append("byteCodeSize", definitionsBytesSize() );
		
		return s.toString();
	}
	
	private int definitionsBytesSize() {
		int bytes = 0;
		classDefinitions.entrySet();
		for( Entry<String, ClassDefinition> entry: classDefinitions.entrySet() ) {
			ClassDefinition cd = entry.getValue();
			bytes += cd.getDefinition().length;
		}
		return bytes;
	}
	
/*
	public void writeExternal(ObjectOutput out) throws IOException {
		if( serializedObject == null && instance != null) {
			serializedObject = serializer.serialize( instance );			
		} else if( serializedObject == null ) {
			throw new NullPointerException( "Cannot serialize because serialized object is null and instance is not set.");
		}
		out.write( (int) serializedObject.length );
		out.write( serializedObject );
		out.writeBoolean( classDefinitions != null );
		if( classDefinitions != null ) { 
			out.writeObject( classDefinitions );
		}
		
		out.writeBoolean( template != null );
		if( template != null ) {
			out.writeObject( template );
		}
		
		out.writeBoolean( lease != null );
		if( lease != null ){
			out.writeObject( lease );
		}
		
		
	}

	public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
		int bLen = in.readInt();
		serializedObject = new byte[bLen];
		int c = 0;
		while( c < bLen ) {
			c+= in.read( serializedObject, c, bLen - c );
		} 
		boolean cd = in.readBoolean();
		if( cd ) {
			classDefinitions = (LinkedHashMap)in.readObject();			
		}
		boolean t = in.readBoolean();
		if( t ) {
			template = (Template) in.readObject();
		}
		boolean l = in.readBoolean();
		if( l ) {
			lease = (ServerLease) in.readObject();
		}
	}*/
}
